home *** CD-ROM | disk | FTP | other *** search
/ Ambrosia Times / AT_1.1_5.3_DOCMaker.sit / The Ambrosia Times 5.3.rsrc / TEXT_140.txt < prev    next >
Text File  |  1998-09-25  |  9KB  |  151 lines

  1.  
  2.  
  3. Bitwise Operator
  4. by Matt Slot
  5.  
  6. This month I want to discuss probably the most important programming practice I've adopted: internal
  7. state validation and error propogation. If you've read the books "Code Complete" and "Writing Solid
  8. Code" from Microsoft Press (or work in the same code base as someone who has), you may already
  9. be familiar with these concepts -- for you, I'm going to evangelize the techniques and provide some
  10. useful snippets that will help you get started.
  11.  
  12. The primary concept is that every possible compile or run time error should be flagged as soon as
  13. possible in the development process. By adding extra sanity checks and wrapping library functions,
  14. you wil reduce the number of stupid mistakes in an implementation, identify run-time errors faster, and
  15. cut your overall debugging time.
  16.  
  17. Code Validation
  18.  
  19. Whenever you write code, you make numerous assumptions about the inputs and run-time
  20. environment. Of course, while you're implementing it, the code is crystal clear and you've added
  21. plenty of comments: around specific lines, for code blocks, above whole functions, and even in the
  22. source or API header file.
  23.  
  24. But any real project takes months to complete, may have several new programmers, and require
  25. several stages of evaluation and redesign. In such an environment, it's quite easy for NULL pointers
  26. to propogate down or error codes to get brushed aside over time. This is a frequent source of obscure
  27. or hard-to-reproduce errors ("well, it works on my machine").
  28.  
  29. Generous use of parameter validation and internal validation is a good way to reduce such
  30. "evolutionary" problems. For each function, check every parameter that is passed against NULL or
  31. valid range of values. Every time a global variable is accessed, validate that it was properly initialized.
  32. For functions that must be called in a specific order, test that the process is performed properly
  33. (typically tracking a state variable).
  34.  
  35. Error Propogation
  36.  
  37. Another way for problems to crop up are unexpected errors returned from library functions. Most
  38. developers quickly write up and unit test a batch of functions to "bootstrap" a specific feature, then go
  39. back and implement better error testing when everything works. In such a case, it's easy to disregard a
  40. reported error or fail to propogate it to the caller.
  41.  
  42. It's important to test the result of *every* function which can fail, even those that should simply never
  43. fail. For example, printf() can fail due to disk full errors (yes, it does happen). When I was first
  44. implementing such tests, I wrapped a call to the MacOS function Dequeue() (which returns an error if
  45. the specified element isn't found) to remove the first element. Obviously it should work as long as
  46. there is an element in the lsit, but in the end, I saved several hours of debugging time because the error
  47. check caught me passing the *address* of the element pointer.
  48.  
  49. Functions can be grouped into 4 categories. The first kind performs an action which may fail in the
  50. course of normal operation (allocating memory, writing to disk); such functions should always inform
  51. the caller if it fails. The second kind are functions which never fail (deallocating memory, zeroing a
  52. block of memory); these functions are typically declared void.
  53.  
  54. An abstraction layer is composed of functions which "wrap" another library, remapping parameters
  55. and error codes from one range (system-defined errors) into another (application-defined errors).
  56. Finally, event handler functions invoke several other functions (which may or may not fail), but have
  57. to handle the user's request from start to finish. This means that it anticipates any errors and displays
  58. an appropriate message ("couldn't save file, disk is full"), but doesn't pass it up because the problem
  59. has been handled.
  60.  
  61. Writing an Error Library
  62.  
  63. While it's great to write implement robust error checking and code validation in the source, it's a drag
  64. because of the impact on performance (not to mention spurious error messages). For this reason, it's
  65. wise to compile 2 entirely different versions of my application or library, one for debugging (larger
  66. and slower) and one for shipping (smaller and faster).
  67.  
  68. Now, writing 2 separate implementations is simply a waste of time, so most programmers compile the
  69. code base twice. Using the preprocessor to define DEBUG let's them distinguish between versions, so
  70. that each version is identical save the error code.
  71.  
  72. In the process of experimenting with rigorous error checking, I implemented several macros to aid
  73. testing and propogation. Because I'm a C programmer who likes a few C++-isms, I actually adopted
  74. terminology and design similar to the throw/catch metaphor.
  75.  
  76. Like the standard C library, my error library contains an Assert function for performing sanity
  77. checking of function parameters and internal state. Because such problems quickly appear during the
  78. implementation and unit testing process, Assert exists only in the DEBUG library and compiles out to
  79. an empty declaration in the non-DEBUG version. However, due to the importance of such problems,
  80. an Assert will force the application to quit immediately (and spur the programmer to fix it on the spot).
  81.  
  82. Next is the Throw declaration, which aborts execution of the current function by jumping to cleanup
  83. code at the end (using the nasty goto construct). This is useful for functions like saving documents,
  84. where a single failure in the process should simply cancel its execution. In the DEBUG version,
  85. throwing an error displays a complete error description before aborting, but unlike the Assert,
  86. non-DEBUG versions still perform the test and abortive cleanup (an error saving a file needs to be
  87. handled, even in shipping applications).
  88.  
  89. While some errors indicate critical problems with program execution, others can be considered "soft
  90. errors". For example, after exchanging some network data an application normally releases the
  91. network endpoint, however on some systems the function to do that returns an error code. By
  92. wrapping the call with a Trace declaration, the DEBUG version displays an error message without
  93. affecting program flow, but because the effect is generally benign the non-DEBUG application remains
  94. silent.
  95.  
  96. Finally, in an abstraction layer, the key is simply determining the error code and how that value should
  97. map into the application's own numbering scheme. For this reason, I added the Remap declaration
  98. which works the exactly the same as Throw except that it reports two error values in the DEBUG
  99. version -- the old (system) and the new (remapped) code.
  100.  
  101. Basically, then, we have several sets of functions which can be sprinkled through a source file, which
  102. will perform several types of validation and error propogation in DEBUG mode, but compile into
  103. simplified handlers for non-DEBUG binaries.
  104.  
  105. To make these 4 types of functions more useful (and more readable), I've implemented variations that
  106. flag NULL pointers, true conditions, or false (zero) conditions. Finally, a Catch declaration is placed
  107. at the end of the function, right before cleanup code, as the target for abortive Throw declarations.
  108.  
  109. Sample Implementation
  110.  
  111. I have placed some sample code online, consisting of: 
  112.  
  113.        http://www.AmbrosiaSW.com/~fprefect/bitwise/stddebug.h
  114.  
  115.        The main set of declarations, consisting of macros that wrap a standard Debug()
  116.        function. These macros record the affected line and file, error code (or codes), a brief
  117.        error description, and the desired behavior (to assert, throw, or trace).
  118.  
  119.        http://www.AmbrosiaSW.com/~fprefect/bitwise/stddebug.c
  120.        http://www.AmbrosiaSW.com/~fprefect/bitwise/macdebug.c
  121.  
  122.        Each contains an implementation of Debug() appropriate to the platform. The standard
  123.        file uses printf() to display the information to stderr, and the MacOS version uses
  124.        DebugStr() to record a message in Macsbug. Feel free to write your own version of
  125.        these functions, to record to file or display error dialogs -- but keep in mind that an
  126.        application may call this function repeatedly while propogating an error up the calling
  127.        stack, or even from software interrupt time.
  128.  
  129. There are several tricks used in this implementation. First, because it's convenient to simply wrap
  130. error-prone functions with the Throw or Trace macro, we have to be careful not to evaluate the
  131. conditional twice. For this reason, we declare a temporary error placeholder and use that through the
  132. rest of the declaration.
  133.  
  134. Given that we needed a temporary variable declaration, we take advantage of a clever C construct. It
  135. executes exactly once, lets us declare a variable within the braces, and even avoids the nested if-else
  136. problem.
  137.  
  138.        #define qMyMacro(x) do { long y; if (y=(x)) DoSomething(y); }
  139.        while(0); if (SimpleFunction()) MyMacro(1); else MyMacro(0); 
  140.  
  141. Anyway, you get the basic idea. I hope this inspires you to implement some rigorous error checking,
  142. and to cut your debugging time dramatically. One thing that I'd really like to see is an implementation
  143. that propogates a complete error structure instead of a simple value, much like the actual C++
  144. throw/catch implementation.
  145.  
  146. Matt Slot, Bitwise Operator
  147.  
  148.    
  149.  
  150.  
  151.